/* Copyright (c) 2007-2008, Jeffrey R Griffin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Jeffrey R Griffin nor the
*       names of his contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY Jeffrey R Griffin ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package net.visibleblue.android.net;

import jarify.ModifiedJarVerifier;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import net.visibleblue.android.paranoid.Alice;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import anon.crypto.IMyPublicKey;
import anon.crypto.JAPCertificate;
import anon.crypto.PKCS7SignedData;

/**
 * URLApkPackageManager class provides a ClassLoader for, and caches a package
 * from a URL specified location.  The cache will be deleted when finalize is called.
 *
 */
public class URLApkPackageHelper {
	
	public static final int CONNECTED_MSG = 0;
	public static final int PACKAGE_LOAD_MSG = 1;
	public static final int PACKAGE_VERIFY_MSG = 2;
	public static final int UNSIGNED_PACKAGE_ARG = 0;
	public static final int PACKAGE_VERIFIED_ARG = 1;
	public static final int PACKAGE_NOT_VERIFIED_ARG = 2;
	
	private String _tempActivityName = null;
	private ClassLoader _encapsulatedClassLoader = null;
	private String _fileName = null;
	private String _className = null;
	private boolean _packageLoaded = false;
	private URLConnection _connection = null;
	private URL _apkURL = null;
	private Context _context = null;
	private Alice _alice = null;

	/**
	 * @param apkUrl full jar-formatted URL including the entry-point class.
	 * At this time, only classes inheriting DynamicActivity will be loaded.
	 */
	public URLApkPackageHelper(URL apkURL, Context context)
	{
		_context = context;
		_apkURL = apkURL;
	}
	
	public void Connect() throws IOException
	{
		JarURLConnection jarURLConn = (JarURLConnection)_apkURL.openConnection();
		_className = jarURLConn.getEntryName();
		_fileName = new File(jarURLConn.getJarFileURL().getFile()).getName();
		_connection = jarURLConn.getJarFileURL().openConnection();
		_connection.connect();
	}

	public void Connect(Handler msgHandler) throws IOException
	{
		Connect();
		Message msg = new Message();
		msg.what = CONNECTED_MSG;
		msgHandler.sendMessage(msg);
	}

	private final void NioFileWrite(InputStream in, FileOutputStream out, int contentLength,
			Handler msgHandler)	throws IOException{
		//ReadableByteChannel readableByteChannel = Channels.newChannel(in);
		//FileChannel fileChannel = out.getChannel();
		byte[] buffer = new byte[16384];
		int pos = 0;
		while(true)
		{
			int num = in.read(buffer, 0, buffer.length);
			if(num==-1)
				break;
			out.write(buffer, 0, num);
			pos+=num;
			
		//a bug in nio transfers prevents us from doing it this way
		/*long downloaded = 0;
		while (downloaded < contentLength) {
			try
			{
			downloaded += fileChannel.transferFrom(readableByteChannel, downloaded, 
					contentLength - downloaded);
			}
			catch(Exception e)
			{
				try{Thread.sleep(1);}
				catch(Throwable t){}
			}*/
			if(msgHandler!=null)
			{
				Message msg = new Message();
				msg.arg1 = (int)pos;
				msg.arg2 = contentLength;
				msg.what = PACKAGE_LOAD_MSG;
				msgHandler.sendMessage(msg);
			}
		}
		out.flush();
		//readableByteChannel.close();
		//fileChannel.close();
	}

	private boolean verifyAlias(JAPCertificate blockCert, File jarFile) {
		_alice = 
			Alice.createFromNonSecureMedium(blockCert.getPublicKey().getEncoded(), _context);
		if(_alice==null)
			throw new RuntimeException("This package's entity cannot be found.");
		IMyPublicKey mpk = blockCert.getPublicKey();
		//the following code gets our stored public key, but as long as we can instantiate
		//alice, the public key stored in the certificate matches our key digest
		/*String algorithm = _alice.getPublicKeyAlgorithm();
		if(algorithm.equals("RSA"))
		{
			byte[] pk = _alice.getPublicKeyEncoded();
			if(pk==null)
				throw new RuntimeException("An invalid key is stored for "+_alice.getContactName());
			mpk = MyRSAPublicKey.getInstance(pk);
		}
		else if(algorithm.equals("DSA"))
		{
			DSAPublicKey dsapk = null;
			try
			{dsapk = (DSAPublicKey)_alice.getPublicKey();}
			catch(NoSuchAlgorithmException nsaEx){}
			if(dsapk==null)
				throw new RuntimeException("An invalid key is stored for "+_alice.getContactName());
			
			DSAParameters params = new DSAParameters(dsapk.getParams().getP(),
					dsapk.getParams().getQ(), dsapk.getParams().getG());
			mpk = new MyDSAPublicKey(new DSAPublicKeyParameters(dsapk.getY(), params));
		}
		else
			throw new RuntimeException("Cannot use algorithm "+algorithm);*/
		
		return ModifiedJarVerifier.verify(jarFile, mpk);
	}

	public void LoadPackage(Handler msgHandler) throws IOException {
		if(_connection==null)
			throw new RuntimeException("You must call Connect before calling this method.");

		InputStream is = _connection.getInputStream();
		FileOutputStream out = 
			_context.openFileOutput(_fileName, Context.MODE_WORLD_READABLE);
		NioFileWrite(is, out, _connection.getContentLength(), msgHandler);
		is.close();
		out.close();
	}
	
	public void LoadPackage() throws IOException {
		LoadPackage(null);
	}
	
	public void VerifyPackage(Handler msgHandler) throws IOException
	{
		File file = _context.getFileStreamPath(_fileName);
		boolean signed = false;
		JarInputStream jarFileStream = new JarInputStream(
	            new FileInputStream(file), false);
		JarEntry jarEnt = null;
		byte[] buffer = new byte[1024];
		while ((jarEnt=jarFileStream.getNextJarEntry())!=null) {
			String name = jarEnt.getName().toUpperCase();
			if (name.startsWith("META-INF/") && (name.endsWith("DSA") || name.endsWith("RSA")))
			{	
				//move the archived block file into a buffer
				signed = true;
				ByteArrayOutputStream blockBuffer = new ByteArrayOutputStream();
				int n = -1;
				while((n = jarFileStream.read(buffer, 0, buffer.length))!=-1)
				{
					blockBuffer.write(buffer, 0, n);
				}
				blockBuffer.close();
				PKCS7SignedData block = null;
				try
				{
					block = new PKCS7SignedData(blockBuffer.toByteArray());
				}
				catch(NoSuchAlgorithmException nsaEx){}
				catch(InvalidKeyException ikEx){}

				//verify the package with the stored key for the entity identified 
				//with this certificate
				if(!verifyAlias(block.getSigningCertificate(), file))
				{
					if(msgHandler!=null)
					{
						Message msg = new Message();
						msg.arg1 = PACKAGE_NOT_VERIFIED_ARG;
						msg.what = PACKAGE_VERIFY_MSG;
						msgHandler.sendMessage(msg);
					}
					else throw new RuntimeException("Cannot verify package.");
				}
				else if(msgHandler!=null)
				{
					Message msg = new Message();
					msg.arg1 = PACKAGE_VERIFIED_ARG;
					msg.what = PACKAGE_VERIFY_MSG;
					msgHandler.sendMessage(msg);
					jarFileStream.closeEntry();
					_packageLoaded = true;
					break;
				}
			}
			jarFileStream.closeEntry();
		}
		jarFileStream.close();
		if(!signed && msgHandler!=null)
		{
			Message msg = new Message();
			msg.arg1 = UNSIGNED_PACKAGE_ARG;
			msg.what = PACKAGE_VERIFY_MSG;
			msgHandler.sendMessage(msg);
		}
	}
	
	public void VerifyPackage() throws IOException
	{
		VerifyPackage(null);
	}
	
	private ClassLoader getEncapsulatedClassLoader()
	{
		if(!_packageLoaded)
			throw new RuntimeException("You must load and verify package before calling this accessor.");
		if(_encapsulatedClassLoader==null)
		{
			try
			{
				File file = _context.getFileStreamPath(_fileName);
				Class appLoadersClass = Class.forName("android.app.ApplicationLoaders");
				Method appLoadersClassStaticCtor = appLoadersClass.getMethod("getDefault", new Class[]{});
				Method appLoadersClassLoaderAccessor = appLoadersClass.getMethod("getClassLoader",  
						new Class[] { String.class, ClassLoader.class });

				Object appLoadersObj = appLoadersClassStaticCtor.invoke(null, null);
				_encapsulatedClassLoader = (ClassLoader)appLoadersClassLoaderAccessor.invoke(appLoadersObj, 
						new Object[]{file.getAbsolutePath(), _context.getClassLoader()});
			}
			catch(Exception ex)
			{
				throw new RuntimeException(ex);
			}
		}
		return _encapsulatedClassLoader;
	}
	
	
	/**
	 * @return Class loader for the URL.
	 */
	public ClassLoader getClassLoader(){
		
		return getEncapsulatedClassLoader();
	}
	
	public Alice getAlice(){
		return _alice;
	}
	
	/**
	 * @return Class of the URL specified type.
	 * @throws ClassNotFoundException
	 */
	public Class getURLClass() throws ClassNotFoundException{
		return getClassInPackage(_className);
	}
	
	public Class getClassInPackage(String className)throws ClassNotFoundException{
		return getEncapsulatedClassLoader().loadClass(className);
	}

	/**
	 * @return Absolute path of the URL specified package.
	 * @throws FileNotFoundException
	 */
	public String getCachedPackagePath() throws FileNotFoundException{
		if(!_packageLoaded)
			throw new RuntimeException("You must load and verify package before calling this accessor.");
		return _context.getFileStreamPath(_fileName).getAbsolutePath();
	}

	@Override
	protected void finalize() throws Throwable {
		_context.deleteFile(_fileName);
		super.finalize();
	}
	
	public String getTempActivityName()
	{
		String name = _tempActivityName;
		_tempActivityName = null;
		return name;
	}
	
	public void storeTempActivityName(String name)
	{
		_tempActivityName = name;
	}

}
